
function DnDHelper() {
}

DnDHelper.prototype = {
  disableDragging: function(elements) {
    elements.attr("draggable", false);
    elements.bind("mousedown",
                  function(event) {
                    event.preventDefault();
                    return false;
                  });
  }
};

function CartoonParser() {
  this.m_callbackObject= undefined;
}

CartoonParser.prototype = {
  parseCartoonsFromFeed: function(feed,
                                  callbackObject) {
    this.m_callbackObject = callbackObject;
    var cartoons = undefined;
    if(feed.status == "ok") {
      cartoons = this.parseCartoons(feed);
    }
    else {
      this.forwardError(feed.URL);
    }
    if(cartoons !== undefined &&
       cartoons.cartoons.length > 0) {
      this.forwardCartoons(cartoons);
    }
    else {
      this.forwardError(feed.URL);
    }
  },
  parseCartoons: function(feed) {
    var cartoons = {URL: feed.URL,
                    title: '',
                    lastModified: '',
                    cartoons: []};
    if(feed &&
       feed.status == "ok") {
      cartoons.lastModified = feed.lastModified;
      cartoons.title = feed.title;
      for(var i =0; i < feed.items.length; i++) {
        var cartoon = this.parseCartoon(feed.items[i]);
        if(cartoon !== undefined) {
          cartoons.cartoons.push(cartoon);
        }
      }
    }
    return cartoons;
  },
  parseCartoon: function(feedItem) {
    var cartoon = undefined;
    var cartoonImage = this.determineTheActualCartoon(feedItem);
    if(cartoonImage.imageURL !== '') {
      cartoon = { title: feedItem.title,
                  URL: feedItem.url,
                  date: feedItem.date,
                  imageURL: cartoonImage.imageURL,
                  altText: cartoonImage.altText,
                  titleText: cartoonImage.titleText};
    }
    return cartoon;
  },
  determineTheActualCartoon: function(item){
    // Let's check if there are any images in description
    var cartoonImage = {imageURL: '',
                        altText: '',
                        titleText: ''};
    var images = this.getItemContent(item);
    var imageTag = this.findImageTag(images);
    if(imageTag !== undefined) {
      cartoonImage.imageURL = imageTag.attr("src");
      cartoonImage.altText = imageTag.attr("alt");
      cartoonImage.titleText = imageTag.attr("title");
    }
    if(cartoonImage.imageURL !== '' &&
       item.url.search(/\.(png|jpe?g|gif)/i) != -1) {
      cartoonImage.imageURL = item.url;
    }
    return cartoonImage;
  },
  getItemContent: function(item) {
    var images = $();
    try {
      images = $(item.description, document).filter("img");
    }
    catch(e) { /* Swallow exception */}
    if(images.length === 0) {
      try {
        images = $(item.content, document).find("img");
      }
      catch(e) { /* Swallow exception */}
    }
    if(images.length === 0) {
      try {
        images = $(item.description, document).find("img");
      }
      catch(e) { /* Swallow exception */}
    }
    return images;
  },
  findImageTag: function(images) {
    var imageTag = undefined;
    if(images.length > 0) {
      tempImageTag = $(images.get(0));
      if(tempImageTag.attr("src") !== undefined) {
        imageTag = tempImageTag;
      }
    }
    return imageTag;
  },
  forwardCartoons: function(cartoons) {
    var that = this.m_callbackObject.that;
    var callback = this.m_callbackObject.callback;
    callback.call(that, cartoons);
  },
  forwardError: function(url) {
    var that = this.m_callbackObject.that;
    var callback = this.m_callbackObject.errorCallback;
    callback.call(that, url, "Couldn't find cartoons from this URL. Check internet connectivity.");
  }
};

function MaemoHelper(window,
                     dndHelper) {
  this.C_IMAGE_URL = "../images/maemo-task-switcher.png";

  this.m_window = window;
  this.m_dndHelper = dndHelper;
}

MaemoHelper.prototype = {
  createTaskSwitcher: function(to) {
    var element = this.createTaskSwitcherElement(this.C_IMAGE_URL,
                                                 this.m_window.document);
    var self = this;
    element.click(function() {self.m_window.minimize();});
    to.prepend(element);
  },
  createTaskSwitcherElement: function(imageURL,
                                      document) {
    var html = "<button id=\"maemoTaskSwitcher\">" +
               "<img id=\"maemoTaskSwitcherIcon\" src=\"\" />" +
               "</button>";
    var element = $(html, document);
    var image = element.find("#maemoTaskSwitcherIcon");
    this.m_dndHelper.disableDragging(image);
    image.attr("src", imageURL);
    return element;
  },
  setFontSizeTo: function(elements, fontSize) {
    elements.css("font-size", fontSize);
  }
};

function CartoonSource(url,
                       name,
                       id,
                       active) {
  this.setUrl(url);
  this.setName(name);
  this.setId(id);
  this.setActive(active);
}

CartoonSource.prototype = {
  setId: function(id) {
    this.m_id = id;
  },
  getId: function() {
    return this.m_id;
  },
  setUrl: function(url) {
    this.m_url = url;
  },
  getUrl: function() {
    return this.m_url;
  },
  setName: function(name) {
    this.m_name = name;
  },
  getName: function() {
    return this.m_name;
  },
  setActive: function(active) {
    if(!active) {
      active = false;
    }
    if(active === true ||
       active === 1) {
      this.m_active = 1;
    }
    if(active === false ||
       active === 0) {
      this.m_active = 0;
    }
  },
  getActive: function() {
    return this.m_active;
  },
  getActiveBoolean: function() {
    return (this.m_active ? true : false);
  }
};

function CartoonSourceResultFiller() {
}

CartoonSourceResultFiller.prototype = {
  createCartoonSource: function(row) {
    var cartoonSource = new CartoonSource();
    cartoonSource.setId(row['id']);
    cartoonSource.setUrl(row['url']);
    cartoonSource.setName(row['name']);
    cartoonSource.setActive(row['active']);
    return cartoonSource;
  }
};

function CartoonSourceResultAdapter(receiver) {
  this.m_receiver = receiver;
  this.m_cartoonSourceResultFiller = new CartoonSourceResultFiller();
}

CartoonSourceResultAdapter.prototype = {
  receiveResults: function(tx, results) {
    var cartoonSources = [];
    for (var i = 0; i < results.rows.length; ++i) {
      var row = results.rows.item(i);
      var cartoonSource = this.m_cartoonSourceResultFiller.createCartoonSource(row);
      cartoonSources.push(cartoonSource);
    }
    this.sendToReceiver(cartoonSources);
  },
  sendToReceiver: function(cartoonSources) {
    this.m_receiver.callback.call(this.m_receiver.that,
                                  cartoonSources);
  },
  getReceiver: function() {
    var callbackObject = { that: this,
                           callback: this.receiveResults};
    return callbackObject;
  }
};

function CartoonAdditionAdapter(backend,
                                receiver) {
  this.m_backend = backend;
  this.m_receiver = receiver;
  this.m_cartoonSourceResultFiller = new CartoonSourceResultFiller();
}

CartoonAdditionAdapter.prototype = {
  receiveIdResult: function(tx,
                            results) {
    var id = results.insertId;
    this.m_backend.getCartoonSource(id,
                                    this.cartoonSourceReceiverCallback());
  },
  receiveCartoonSource: function(tx,
                                 results) {
    var row = results.rows.item(0);
    var cartoonSource = this.m_cartoonSourceResultFiller.createCartoonSource(row);
    this.sendToReceiver(cartoonSource);
  },
  sendToReceiver: function(cartoonSource) {
    this.m_receiver.callback.call(this.m_receiver.that,
                                  cartoonSource);
  },
  getReceiver: function() {
    var callbackObject = { that: this,
                           callback: this.receiveIdResult};
    return callbackObject;
  },
  cartoonSourceReceiverCallback: function() {
    var callbackObject = { that: this,
                           callback: this.receiveCartoonSource};
    return callbackObject;
  }
};

function CartoonDataFetcher(feedUpdateBroker,
                            cartoonParser,
                            feedResolver) {
  this.m_feedUpdateBroker = feedUpdateBroker;
  this.m_cartoonParser = cartoonParser;
  this.m_feedResolver = feedResolver;
  this.m_case = undefined;
  this.C_MAX_WAIT_TIME = 60000;
}

CartoonDataFetcher.prototype = {
  abort: function() {
    if(this.m_case !== undefined &&
       "xhr" in this.m_case) {
        this.m_case.xhr.abort();
    }
  },
  getDataFromURL: function(URL,
                           receivers,
                           feedDiscover) {
    if(feedDiscover === undefined) {
      feedDiscover = true;
    }
    this.m_case = this.createCase(URL,
                                  receivers,
                                  feedDiscover);
            this.createXHR(URL);
  },
  createCase: function(URL,
                       receivers,
                       feedDiscover) {
    var xhr = this.createXHR(this,
                             URL,
                             this.C_MAX_WAIT_TIME);
    var caseData = { URL: URL,
                     receivers: receivers,
                     feedDiscover: feedDiscover,
                     xhr: xhr.xhr,
                     timeoutID: xhr.timeoutID
                   };
    return caseData;
  },
  createXHR: function(that,
                      URL,
                      timeout) {
    var receiver = that;
    var log = console;
    var xhr = new XMLHttpRequest();
    xhr.onreadystatechange = function() {
                               log.debug(receiver);
                               if(xhr.readyState === 4 &&
                                  receiver.receiveResponse) {
                                 receiver.receiveResponse(xhr,
                                                          xhr.statusText);
                               }
                             };
    xhr.overrideMimeType("text/xml");
    xhr.open("GET", URL, true);
    xhr.send(null);
    var timeoutID = setTimeout(function() {xhr.abort();}, timeout);
    return { timeoutID: timeoutID, xhr: xhr };
  },
  receiveResponse: function(request,
                            textStatus) {
    clearTimeout(this.m_case.timeoutID);
    var feedInfos = [];
    this.m_case.status = textStatus;
    if(this.m_case.feedDiscover &&
       this.m_feedResolver !== undefined) {
      feedInfos = this.m_feedResolver.findFeedInfos(request);
    }
    if(feedInfos.length > 0) {
      this.getDataFromURL(feedInfos[0].href,
                          this.m_case.receivers,
                          false);
      return;
    }
    else if(feedInfos.length == 0 &&
            request.responseXML) {
      this.processFeed(request,
                       this.m_case.receivers.cartoonReceiver);
    }
    else {
      this.reportError(this.m_case);
    }
  },
  processFeed: function(request,
                        receiver) {
    var feed = this.m_feedUpdateBroker.handleResponse(request,
                                                      request.status,
                                                      request.responseXML);
    this.m_cartoonParser.parseCartoonsFromFeed(feed, receiver);
  },
  reportError: function(errorCase) {
    try {
      var errorReceiver = errorCase.receivers.errorReceiver;
      errorReceiver.callback.call(errorReceiver.that,
                                  errorCase.URL,
                                  "Couldn't find cartoons for URL " + 
                                  errorCase.URL +
                                  "(because of " + errorCase.status +
                                  ").");
    }
    catch(e) {
      alert("CartoonDataFetcher.reportError: Error callback threw an exception: " + e);
    }
  }
};

function FeedResolver() {
      /*
       * https://developer.mozilla.org/en/Core_JavaScript_1.5_Reference/Global_Objects/RegExp
       *                   <link (whitespace|rel="alternate"|type="application/(atom|rss)+xml"|title="*"|href="*") />
       */
  this.LINK_MATCH_REGEX = /<link(?:\s+|(?:rel=[\"\']alternate[\"\']){1}|(?:(type)=[\"\']application\/(atom|rss)\+xml[\"\']){1}|(?:(title){1}=[\"\']([^<&\"]*)[\"\'])?|(href){1}=[\"\']([^<&\"]*)[\"\'])*\s*\/>/i;
}

FeedResolver.prototype = {
  findFeedInfos: function(response) {
    var feedInfos = [];
    if(response.responseXML) {
      feedInfos = this.findFeedInfosFromDOM(response.responseXML);
    }
    if(!response.responseXML &&
       response.responseText) {
      feedInfos = this.findFeedInfosFromText(response.responseText);
    }
    return feedInfos;
  },
  findFeedInfosFromDOM: function(xmlDoc) {
    var feedInfos = [];
    if(xmlDoc.getElementsByTagName("html").length > 0) {
      var elements = xmlDoc.getElementsByTagName("link");
      var baseURL = xmlDoc.baseURI;
      for(var i = 0; i < elements.length; i++) {
        var attributes = { rel:  elements[i].rel.toLowerCase(),
                           title: elements[i].title,
                           type: elements[i].type.toLowerCase()
                                                 .replace(/application\/([a-z]+)\+xml/g,
                                                          "$1"),
                           href: elements[i].href};
        if(attributes.rel === 'alternate' &&
           (attributes.type === 'atom' ||
            attributes.type === 'rss') &&
           attributes.href !== '') {
          delete attributes.rel;
          feedInfos.push(attributes);
        }
      }
    }
    return feedInfos;
  },
  findFeedInfosFromText: function(htmlText) {
    var feedInfos = [];
    if(htmlText.search(/<html.*?>/i) != -1) {
      var beforeHead = htmlText.split(/<\/head>/i)[0];
      var textArray = beforeHead.replace(/\/?>/g, "/>\n").split(/\n\r?/);
      for(var i=0; i<textArray.length; i++) {
        var feedInfo = this.findSingleFeedInfoFromText(textArray[i]);
        if(feedInfo) {
          feedInfos.push(feedInfo);
        }
      }
    }
    return feedInfos;
  },
  findSingleFeedInfoFromText: function(text) {
    var matches = this.LINK_MATCH_REGEX.exec(text);
    var feedInfo = undefined;
    if(matches) {
      feedInfo = {};
      for(var i=0; i<matches.length; i++) {
        if(matches[i] !== undefined &&
          (matches[i].toLowerCase() === 'type' ||
           matches[i].toLowerCase() === 'href' ||
           matches[i].toLowerCase() === 'title')) {
          feedInfo[matches[i]] = matches[i+1];
          i = i+2;
          continue;
        }
      }
    }
    return feedInfo;
  }
};

function CartoonManagerBackend(db) {
  this.m_db = db;
  this.m_txId = 0;
  this.m_resultReceivers = {};
  this.C_TABLE_NAME = "CartoonReaderSources";
  this.C_DEFAULT_FEED_NAME = "xkcd";
  this.C_DEFAULT_FEED = "http://xkcd.com/atom.xml";
  this.C_CREATE_TABLE_SQL = "CREATE TABLE IF NOT EXISTS " + this.C_TABLE_NAME + " " +
                            "(id INTEGER PRIMARY KEY AUTOINCREMENT," +
                            " url TEXT, " +
                            " name TEXT, " +
                            " active INTEGER NOT NULL DEFAULT 0);";
  this.setup();
}

CartoonManagerBackend.prototype = {
  setup: function() {
    this.executeSQL(this.C_CREATE_TABLE_SQL,
                    [],
                    this.getEmptyHandler());
  },
  getEmptyHandler: function() {
    var handler = {that: this,
                   callback: this.emptyHandler};
    return handler;
  },
  emptyHandler: function(tx, result) {
    console.log("CartoonManagerBackend.emptyHandler called.");
  },
  registerAddCartoonSourceAdapter: function(adapter) {
    this.m_addCartoonSourceAdapter = adapter;
  },
  registerGetCartoonSourcesAdapter: function(adapter) {
    this.m_getCartoonSourcesAdapter = adapter;
  },
  registerGetCartoonSourceAdapter: function(adapter) {
    this.m_getCartoonSourceAdapter = adapter;
  },
  registerUpdateCartoonSourceAdapter: function(adapter) {
    this.m_updateCartoonSourceAdapter = adapter;
  },
  getCartoonSources: function(receiver) {
    if(receiver === undefined &&
       this.m_getCartoonSourcesAdapter !== undefined) {
      receiver = this.m_getCartoonSourcesAdapter.getReceiver();
    }
    if(receiver === undefined) {
      receiver = this.getEmptyHandler();
    }
    var cartoonSourceSQL = "SELECT * FROM " + this.C_TABLE_NAME + ";";
    this.executeSQL(cartoonSourceSQL,
                    [],
                    receiver);
  },
  getCartoonSource: function(id,
                             receiver) {
    if(receiver === undefined &&
       this.m_getCartoonSourceAdapter !== undefined) {
      receiver = this.m_getCartoonSourceAdapter.getReceiver();
    }
    if(receiver === undefined) {
      receiver = this.getEmptyHandler();
    }
    var cartoonSourceForIdSQL = "SELECT * FROM " + this.C_TABLE_NAME + " " +
                                "WHERE id = ?;";
    this.executeSQL(cartoonSourceForIdSQL,
                    [id],
                    receiver);
  },
  getActiveCartoonSource: function(receiver) {
    if(receiver === undefined &&
       this.m_getCartoonSourceAdapter !== undefined) {
      receiver = this.m_getCartoonSourceAdapter.getReceiver();
    }
    if(receiver === undefined) {
      receiver = this.getEmptyHandler();
    }
    var activeCartoonSourceSQL = "SELECT * FROM " + this.C_TABLE_NAME + " " +
                                 "WHERE active = 1;";
    this.executeSQL(activeCartoonSourceSQL,
                    [],
                    receiver);
  },
  addCartoonSource: function(cartoonSource,
                             receiver) {
    if(receiver === undefined &&
       this.m_addCartoonSourceAdapter !== undefined) {
      receiver = this.m_addCartoonSourceAdapter.getReceiver();
    }
    if(receiver === undefined) {
      receiver = this.getEmptyHandler();
    }
    var createSQL = "INSERT INTO " + this.C_TABLE_NAME + " " +
                    "(url, name, active) VALUES (?, ?, 0);";
    this.executeSQL(createSQL,
                    [cartoonSource.getUrl(),
                     cartoonSource.getName()],
                    receiver);
  },
  receiveModelChanges: function(changes) {
    if("toBeAdded" in changes) {
      this.handleAdded(changes.toBeAdded);
    }
    if("updated" in changes) {
      this.handleUpdated(changes.updated);
    }
    if("removed" in changes) {
      this.handleRemoved(changes.removed);
    }
  },
  handleAdded: function(added) {
    this.addCartoonSource(added.cartoonSource);
  },
  handleUpdated: function(updated) {
    if("cartoonSources" in updated) {
      for(var i = 0; i < updated.cartoonSources.length; i++) {
        this.updateCartoonSource(updated.cartoonSources[i]);
      }
    }
  },
  updateCartoonSource: function(cartoonSource) {
    var receiver = this.getEmptyHandler();
    if(this.m_updateCartoonSourceAdapter !== undefined) {
      receiver = this.m_updateCartoonSourceAdapter.getReceiver();
    }
    var updateSQL = "UPDATE " + this.C_TABLE_NAME + " " +
                    "SET url = ?, " +
                        "name = ?, " +
                        "active = ? " +
                    "WHERE id = ?;";
    this.executeSQL(updateSQL,
                    [cartoonSource.getUrl(),
                     cartoonSource.getName(),
                     cartoonSource.getActive(),
                     cartoonSource.getId()],
                    receiver);
  },
  handleRemoved: function(removed) {
    var removeSQL = "DELETE FROM " + this.C_TABLE_NAME + " "+
                    "WHERE id = ?;";
    this.executeSQL(removeSQL,
                    [removed.getId()],
                    this.getEmptyHandler());
  },
  executeSQL: function(SQL,
                       params,
                       resultReceiver) {
    var txId = this.m_txId;
    this.m_txId++;
    var self = this;
    this.m_resultReceivers[txId] = resultReceiver;
    this.m_db.transaction(function(tx) {
                            tx.txId = txId;
                            tx.SQL = SQL;
                            tx.executeSql(SQL,
                                          params,
                                          function(tx, result) {self.generalResultReceiver(tx, result);},
                                          function(tx, error) {self.generalErrorReceiver(tx, error);});
                          });
  },
  generalResultReceiver: function(tx, result) {
    /** Let's cheat a bit and insert default if none was
        was returned... */
    if(tx.SQL.search(/SELECT/) != -1 &&
       result.rows.length == 0) {
      // Insert default
      var insertDefault = "INSERT INTO " + this.C_TABLE_NAME + " " +
                       "(url, name, active) VALUES(?, ?, ?);";
      this.executeSQL(insertDefault,
                      [this.C_DEFAULT_FEED, this.C_DEFAULT_FEED_NAME, 1],
                      this.getEmptyHandler());
      this.executeSQL(tx.SQL,
                      [],
                      this.m_resultReceivers[tx.txId]);
      delete this.m_resultReceivers[tx.txId];
      return;
    }
    var receiver = this.m_resultReceivers[tx.txId];
    try {
      receiver.callback.call(receiver.that, tx, result);
    }
    catch(e) {
      alert("CartoonManagerBackend failed to call the result receiver because: " + e);
    }
  },
  generalErrorReceiver: function(tx, error) {
    alert("CartoonManagerBackend received error when trying to execute SQL: " + error.message);
  }
};

function ObjectSpiller(recurse) {
  //var m_recursionDepth = 0;
  this.m_recurseOn = recurse;
}

ObjectSpiller.prototype = {
  getRecurseOn: function() {
    return this.m_recurseOn;
  },
  setRecurseOn: function(recurseOn) {
    this.m_recurseOn = recurseOn;
  },
  vomit: function(object) {
    for(var name in object) {
      alert(name + " : " + object[name]);
      if(typeof(object[name]) == "object" &&
         this.m_recurseOn) {
        alert("Recursing on " + name);
        this.vomit(object[name]);
      }
    }
  }
};
